﻿// ============================================================================
// FileName: CallManager.cs
//
// Description:
// Manages established calls.
//
// Author(s):
// Aaron Clauson
//
// History:
// 10 Feb 2008  Aaron Clauson   Created.
//
// License: 
// This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) 2008 Aaron Clauson (aaronc@blueface.ie), Blue Face Ltd, Dublin, Ireland (www.blueface.ie)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
// the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
// disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd. 
// nor the names of its contributors may be used to endorse or promote products derived from this software without specific 
// prior written permission. 
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
// POSSIBILITY OF SUCH DAMAGE.
// ============================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Net;
using System.Text;
using BlueFace.Sys;
using BlueFace.Sys.Net;
using BlueFace.VoIP.Net;
using BlueFace.VoIP.Net.SIP;
using BlueFace.VoIP.SIPServer;
using log4net;

namespace BlueFace.VoIP.SIPServerCores.StatefulProxy
{
    public delegate void DialogueBridgeCreatedDelegate(SIPDialogue clientDialogue, SIPDialogue forwardedDialogue, string owner);
    public delegate void DialogueBridgeClosedDelegate(string dialogueId, string owner);
    
    public class CallManager
    {
        private static ILog logger = AppState.logger;

        private static string m_tagParameterName = SIPHeaderAncillary.SIP_HEADERANC_TAG;
        private static string m_remoteHangupCause = SIPConstants.SIP_REMOTEHANGUP_CAUSE;

        //public event DialogueBridgeCreatedDelegate BridgeCreated;
        //public event DialogueBridgeClosedDelegate BridgeClosed;

        private SIPTransport m_sipTransport;
        private IPEndPoint m_defaultContact;
        private ProxyLogDelegate m_logDelegate;

        private Dictionary<string, SIPDialogue> m_dialogues = new Dictionary<string, SIPDialogue>();    // <Dialogue Id, SIPDialogue>.
        private Dictionary<string, string> m_dialogueOwner = new Dictionary<string, string>();          // <SIPDialogue, Owner>.
        private Dictionary<string, string> m_dialogueBridges = new Dictionary<string, string>();        // <Dialogue Id, Dialogue Id>.
        private Dictionary<string, string> m_inDialogueTransactions = new Dictionary<string, string>(); // <Forwarded transaction id, Origin transaction id>.
        private Dictionary<string, string> m_reInvitedDialogues = new Dictionary<string, string>();     // <Dialogue Id being reinvited, Replacement Dialogue Id>.

        public CallManager(SIPTransport sipTransport, IPEndPoint defaultContact, ProxyLogDelegate logDelegate)
        {
            m_sipTransport = sipTransport;
            m_defaultContact = defaultContact;
            m_logDelegate = logDelegate;
        }

        public void CreateDialogueBridge(SIPDialogue clientDiaglogue, SIPDialogue forwardedDialogue, string owner)
        {
            Guid bridgeId = Guid.NewGuid();
            clientDiaglogue.CDR.BridgeId = bridgeId;
            forwardedDialogue.CDR.BridgeId = bridgeId;

            AddDialogue(clientDiaglogue, owner);
            AddDialogue(forwardedDialogue, owner);
            
            lock (m_dialogueBridges)
            {
                if (!m_dialogueBridges.ContainsKey(clientDiaglogue.DialogueId))
                {
                    m_dialogueBridges.Add(clientDiaglogue.DialogueId, forwardedDialogue.DialogueId);
                }
                else
                {
                    throw new ApplicationException("Could not add a dialogue Id to a bridge because the key was already present.");
                }

                if (!m_dialogueBridges.ContainsKey(forwardedDialogue.DialogueId))
                {
                    m_dialogueBridges.Add(forwardedDialogue.DialogueId, clientDiaglogue.DialogueId);
                }
                else
                {
                    throw new ApplicationException("Could not add a dialogue Id to a bridge because the key was already present.");
                }
            }
        }

        public void CallHungup(SIPDialogue sipDialogue, string hangupCause)
        {
            try
            {
                if (m_dialogueBridges.ContainsKey(sipDialogue.DialogueId))
                {
                    string orphanedDialogueId = m_dialogueBridges[sipDialogue.DialogueId];
                    string callOwner = (m_dialogueOwner.ContainsKey(sipDialogue.DialogueId)) ? m_dialogueOwner[sipDialogue.DialogueId] : null;

                    SIPDialogue orphanedDialogue = m_dialogues[orphanedDialogueId];

                    // Update CDR's.
                    SIPCDR hungupCallCDR = sipDialogue.CDR;
                    SIPCDR orphanedCallCDR = orphanedDialogue.CDR;

                    hungupCallCDR.Hungup(hangupCause);
                    orphanedCallCDR.Hungup(m_remoteHangupCause);

                    orphanedDialogue.Hangup();

                    lock (m_dialogues)
                    {
                        m_dialogues.Remove(sipDialogue.DialogueId);
                        m_dialogues.Remove(orphanedDialogueId);
                    }

                    lock (m_dialogueBridges)
                    {
                        m_dialogueBridges.Remove(sipDialogue.DialogueId);
                        m_dialogueBridges.Remove(orphanedDialogueId);
                    }

                    lock (m_dialogueOwner)
                    {
                        m_dialogueOwner.Remove(sipDialogue.DialogueId);
                        m_dialogueOwner.Remove(orphanedDialogueId);
                    }
                }
                else
                {
                    logger.Warn("No bridge could be found for hungup call.");
                }

                //DumpStoredDialogues();
            }
            catch (Exception excp)
            {
                logger.Error("Exception CallHungup. " + excp.Message);
            }
        }

        public SIPDialogue GetDialogue(string callId, string localTag, string remoteTag, IPEndPoint remoteEndPoint)
        {
            try
            {
                string dialogueId = SIPDialogue.GetDialogueId(callId, localTag, remoteTag);

                if (m_dialogues.ContainsKey(dialogueId))
                {
                    return m_dialogues[dialogueId];
                }
                else
                {
                    //logger.Warn("The CallManager could not locate a dialogue based on the dialogue id. Checking local tag only.");

                    // Try on To tag.
                    lock (m_dialogues)
                    {
                        foreach (SIPDialogue dialogue in m_dialogues.Values)
                        {
                            if (dialogue.LocalUserField.Parameters.Get(m_tagParameterName) == localTag)
                            {
                                logger.Warn("CallManager dialogue match found on To tag (" + remoteEndPoint.ToString() + ").");
                                return dialogue;
                            }
                        }
                    }

                    // Try on From tag.
                    lock (m_dialogues)
                    {
                        foreach (SIPDialogue dialogue in m_dialogues.Values)
                        {
                            if (dialogue.RemoteUserField.Parameters.Get(m_tagParameterName) == remoteTag)
                            {
                                logger.Warn("CallManager dialogue match found on From tag (" + remoteEndPoint.ToString() + ").");
                                return dialogue;
                            }
                        }
                    }

                    // As an experiment will try on the Call-ID as well. However as a safeguard it will only succeed if there is only one instance of the
                    // Call-ID in use. Since the Call-ID is not mandated by the SIP standard as being unique there it may be that matching on it causes more
                    // problems then it solves.
                    SIPDialogue matchingDialogue = null;
                    lock (m_dialogues)
                    {
                        foreach (SIPDialogue dialogue in m_dialogues.Values)
                        {
                            if (dialogue.CallId == callId && IsCallIdUniqueForDialogues(callId))
                            {
                                logger.Info("The Call-ID match mechanism matched in the CallManager for a request from " + remoteEndPoint + ".");
                                return dialogue;
                            }
                        }
                    }

                    return null;
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception GetDialogue. " + excp);
                return null;
            }
        }

        /// <summary>
        /// Retrieves the other end of a call given the dialogue from one end.
        /// </summary>
        /// <param name="dialogue"></param>
        /// <returns></returns>
        public SIPDialogue GetOppositeDialogue(SIPDialogue dialogue)
        {
            if (m_dialogueBridges.ContainsKey(dialogue.DialogueId))
            {
                return m_dialogues[m_dialogueBridges[dialogue.DialogueId]];
            }

            return null;
        }

        /// <summary>
        /// Attempts to reinvite an existing end of a call by sending a new SDP.
        /// </summary>
        /// <param name="dialogue">The dialogue describing the end of the call to be re-invited.</param>
        /// <param name="newSDP">The session description for the new dialogue desired.</param>
        public void ReInvite(SIPDialogue dialogue, SIPDialogue replacementDialogue)
        {
            try
            {
                m_dialogues.Add(replacementDialogue.DialogueId, replacementDialogue);
                m_reInvitedDialogues.Add(dialogue.DialogueId, replacementDialogue.DialogueId);
                IPEndPoint fromEndPoint = m_sipTransport.GetDefaultTransportContact(SIPProtocolsEnum.UDP);
                SIPRequest reInviteReq = GetInviteRequest(dialogue, fromEndPoint, replacementDialogue.SDP);
                UACInviteTransaction reInviteTransaction = new UACInviteTransaction(m_sipTransport, reInviteReq, reInviteReq.GetRequestEndPoint(), fromEndPoint, SIPProtocolsEnum.UDP);
                reInviteTransaction.UACInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(ReInviteTransactionFinalResponseReceived);
                reInviteTransaction.SendInviteRequest(reInviteReq.GetRequestEndPoint(), reInviteReq);
            }
            catch (Exception excp)
            {
                logger.Error("Exception CallManager ReInvite. " + excp.Message);
                throw excp;
            }
        }

        private void ReInviteTransactionFinalResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            try
            {
                SIPRequest inviteRequest = sipTransaction.TransactionRequest;
                string dialogueId = GetDialogue(inviteRequest.Header.CallId, inviteRequest.Header.From.FromTag, inviteRequest.Header.To.ToTag, remoteEndPoint).DialogueId;
                m_dialogueBridges[dialogueId] = m_reInvitedDialogues[dialogueId];
                m_reInvitedDialogues.Remove(dialogueId);
            }
            catch (Exception excp)
            {
                logger.Error("Exception ReInviteTransactionFinalResponseReceived. " + excp.Message);
                throw excp;
            }
        }

        private void AddDialogue(SIPDialogue dialogue, string owner)
        {
            lock (m_dialogues)
            {
                if (!m_dialogues.ContainsKey(dialogue.DialogueId))
                {
                    m_dialogues.Add(dialogue.DialogueId, dialogue);
                }
                else
                {
                    logger.Warn("Could not add a dialogue Id to a the dialogues list because the key was already present.");
                }
            }

            lock (m_dialogueOwner)
            {
                if (!m_dialogueOwner.ContainsKey(dialogue.DialogueId))
                {
                    m_dialogueOwner.Add(dialogue.DialogueId, owner);
                }
                else
                {
                    logger.Warn("Could not add a dialogue Id to a the dialogue owners because the key was already present.");
                }
            }
        }

        private void DumpStoredDialogues()
        {
            logger.Debug("Stored dialogues:");
            
            foreach(SIPDialogue dialogue in m_dialogues.Values)
            {
                logger.Debug(" dialogue: " + dialogue.DialogueId + ".");
            }

            foreach (string owner in m_dialogueOwner.Values)
            {
                logger.Debug(" owner: " + owner + ".");
            }

            foreach (string bridge in m_dialogueBridges.Values)
            {
                logger.Debug(" bridge: " + bridge + ".");
            }
        }

        public void ForwardInDialogueRequest(SIPDialogue dialogue, SIPTransaction inDialogueTransaction, IPEndPoint localEndPoint, IPEndPoint remoteEndPoint)
        {
            try
            {
                logger.Debug("Forwarding in dialogue request " + inDialogueTransaction.TransactionRequest.Method + " from " + remoteEndPoint + " and call " + inDialogueTransaction.TransactionRequest.URI.ToString() + ".");
                
                // Get the dialogue for the other end of the bridge.
                SIPDialogue bridgedDialogue = m_dialogues[m_dialogueBridges[dialogue.DialogueId]];

                SIPRequest forwardedRequest = inDialogueTransaction.TransactionRequest.Copy();
                forwardedRequest.URI = bridgedDialogue.RemoteTarget;
                forwardedRequest.Header.Routes = bridgedDialogue.RouteSet;
                forwardedRequest.Header.CallId = bridgedDialogue.CallId;
                bridgedDialogue.RemoteCSeq = bridgedDialogue.RemoteCSeq + 1;
                forwardedRequest.Header.CSeq = bridgedDialogue.RemoteCSeq;
                forwardedRequest.Header.To = new SIPToHeader(null, bridgedDialogue.RemoteUserField.URI, bridgedDialogue.RemoteTag);
                forwardedRequest.Header.From = new SIPFromHeader(null, bridgedDialogue.LocalUserField.URI, bridgedDialogue.LocalTag);
                forwardedRequest.Header.Contact = new List<SIPContactHeader>();
                forwardedRequest.Header.Contact.Add(new SIPContactHeader(null, bridgedDialogue.LocalUserField.URI));
                forwardedRequest.Header.Via = new SIPViaSet();
                forwardedRequest.Header.Via.PushViaHeader(new SIPViaHeader(m_defaultContact, CallProperties.CreateBranchId()));
               
                // No authentication for forwarded request.
                forwardedRequest.Header.AuthenticationHeader = null;
                forwardedRequest.Header.WWWAuthenticate = false;
                forwardedRequest.Header.ProxyAuthenticate = false;
                forwardedRequest.Header.ProxyAuthorization = false;
                forwardedRequest.Header.Authorize = false;

                if (inDialogueTransaction.TransactionRequest.Body != null && inDialogueTransaction.TransactionRequest.Method == SIPMethodsEnum.INVITE)
                {
                    bool wasMangled = false;
                    forwardedRequest.Body = SIPPacketMangler.MangleSDP(inDialogueTransaction.TransactionRequest.Body, inDialogueTransaction.TransactionRequest.Header.Via.BottomViaHeader.GetEndPoint().Address.ToString(), out wasMangled);
                    forwardedRequest.Header.ContentLength = forwardedRequest.Body.Length;
                }

                if (inDialogueTransaction.TransactionRequest.Method == SIPMethodsEnum.INVITE)
                {
                    UACInviteTransaction forwardedTransaction = m_sipTransport.CreateUACTransaction(forwardedRequest, forwardedRequest.GetRequestEndPoint(), m_sipTransport.GetTransportContact(null), inDialogueTransaction.Protocol);
                    forwardedTransaction.CDR = null;    // Don't want CDR's on re-INVITES.
                    forwardedTransaction.UACInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(InDialogueTransactionFinalResponseReceived);
                    forwardedTransaction.UACInviteTransactionInformationResponseReceived += new SIPTransactionResponseReceivedDelegate(InDialogueTransactionInfoResponseReceived);
                    forwardedTransaction.TransactionRemoved += new SIPTransactionRemovedDelegate(InDialogueTransactionRemoved);

                    forwardedTransaction.SendReliableRequest();

                    lock (m_inDialogueTransactions)
                    {
                        m_inDialogueTransactions.Add(forwardedTransaction.TransactionId, inDialogueTransaction.TransactionId);
                    }
                }
                else
                {
                    SIPNonInviteTransaction forwardedTransaction = m_sipTransport.CreateNonInviteTransaction(forwardedRequest, forwardedRequest.GetRequestEndPoint(), m_sipTransport.GetTransportContact(null), inDialogueTransaction.Protocol);
                    forwardedTransaction.NonInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(InDialogueTransactionFinalResponseReceived);
                    forwardedTransaction.NonInviteTransactionInfoResponseReceived += new SIPTransactionResponseReceivedDelegate(InDialogueTransactionInfoResponseReceived);
                    forwardedTransaction.TransactionRemoved += new SIPTransactionRemovedDelegate(InDialogueTransactionRemoved);

                    forwardedTransaction.SendReliableRequest();

                    lock (m_inDialogueTransactions)
                    {
                        m_inDialogueTransactions.Add(forwardedTransaction.TransactionId, inDialogueTransaction.TransactionId);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception ForwardInDialogueRequest. " + excp);
            }
        }

        private void InDialogueTransactionInfoResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            try
            {
                // Lookup the originating transaction.
                SIPTransaction originTransaction = m_sipTransport.GetTransaction(m_inDialogueTransactions[sipTransaction.TransactionId]);

                SIPResponse response = sipResponse.Copy();
                response.Header.Via = originTransaction.TransactionRequest.Header.Via;
                response.Header.To = originTransaction.TransactionRequest.Header.To;
                response.Header.From = originTransaction.TransactionRequest.Header.From;
                response.Header.CallId = originTransaction.TransactionRequest.Header.CallId;
                response.Header.CSeq = originTransaction.TransactionRequest.Header.CSeq;
                response.Header.Contact = new List<SIPContactHeader>();
                response.Header.Contact.Add(new SIPContactHeader(null, SIPURI.ParseSIPURI("sip:" + originTransaction.SendFromEndPoint)));

                // Forward the response back to the requester.
                originTransaction.SendInformationalResponse(response);
            }
            catch (Exception excp)
            {
                logger.Error("Exception InDialogueTransactionInfoResponseReceived. " + excp);
            }
        }

        private void InDialogueTransactionFinalResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            try
            {
                logger.Debug("Final response on " + sipResponse.Header.CSeqMethod + " in-dialogue transaction.");
                
                // Lookup the originating transaction.
                SIPTransaction originTransaction = m_sipTransport.GetTransaction(m_inDialogueTransactions[sipTransaction.TransactionId]);

                SIPResponse response = sipResponse.Copy();
                response.Header.Via = originTransaction.TransactionRequest.Header.Via;
                response.Header.To = originTransaction.TransactionRequest.Header.To;
                response.Header.From = originTransaction.TransactionRequest.Header.From;
                response.Header.CallId = originTransaction.TransactionRequest.Header.CallId;
                response.Header.CSeq = originTransaction.TransactionRequest.Header.CSeq;
                response.Header.Contact = new List<SIPContactHeader>();
                response.Header.Contact.Add(new SIPContactHeader(null, SIPURI.ParseSIPURI("sip:" + originTransaction.SendFromEndPoint)));

                if (sipResponse.Body != null && sipResponse.Header.CSeqMethod == SIPMethodsEnum.INVITE)
                {
                    bool wasMangled = false;
                    response.Body = SIPPacketMangler.MangleSDP(sipResponse.Body, sipResponse.Header.Via.BottomViaHeader.GetEndPoint().Address.ToString(), out wasMangled);
                    response.Header.ContentLength = response.Body.Length;
                }

                // Forward the response back to the requester.
                originTransaction.SendFinalResponse(response);
            }
            catch (Exception excp)
            {
                logger.Error("Exception InDialogueTransactionFinalResponseReceived. " + excp);
            }
        }

        private void InDialogueTransactionRemoved(SIPTransaction sipTransaction)
        {
            try
            {
                if (m_inDialogueTransactions.ContainsKey(sipTransaction.TransactionId))
                {
                    lock (m_inDialogueTransactions)
                    {
                        m_inDialogueTransactions.Remove(sipTransaction.TransactionId);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception InDialogueTransactionStateChanged. " + excp);
            }
        }

        private SIPRequest GetInviteRequest(SIPDialogue dialogue, IPEndPoint localEndPoint, string body)
        {
            SIPRequest inviteRequest = new SIPRequest(SIPMethodsEnum.INVITE, dialogue.RemoteTarget);
            SIPProtocolsEnum protocol = inviteRequest.URI.Protocol;

            SIPHeader inviteHeader = new SIPHeader(SIPFromHeader.ParseFromHeader(dialogue.LocalUserField.ToString()), SIPToHeader.ParseToHeader(dialogue.RemoteUserField.ToString()), dialogue.RemoteCSeq++, dialogue.CallId);
            inviteHeader.Contact = SIPContactHeader.ParseContactHeader("sip:" + IPSocket.GetSocketString(localEndPoint));
            inviteHeader.CSeqMethod = SIPMethodsEnum.INVITE;
            inviteRequest.Header = inviteHeader;

            SIPViaHeader viaHeader = new SIPViaHeader(localEndPoint.Address.ToString(), localEndPoint.Port, CallProperties.CreateBranchId(), protocol);
            inviteRequest.Header.Via.PushViaHeader(viaHeader);

            inviteRequest.Body = body;
            inviteRequest.Header.ContentLength = body.Length;
            inviteRequest.Header.ContentType = "application/sdp";

            return inviteRequest;
        }

        /// <summary>
        /// As an experimental method to match otherwise unmatched dialogues an attempt will be made to just use the Call-ID header. Some SIP agents were observed to be
        /// sending in BYEs with a tag missing. However to safeguard against incorrect matching the Call-ID will only match if it is unique in the dialogue list. This method
        /// checks whether that is the case.
        /// </summary>
        /// <param name="callId">The Call-ID to check dialogue uniqueness for.</param>
        /// <returns>True if the Call-ID is unique, false otherwise.</returns>
        private bool IsCallIdUniqueForDialogues(string callId)
        {
            bool match = false;

            lock (m_dialogues)
            {
                foreach (SIPDialogue dialogue in m_dialogues.Values)
                {
                    if (dialogue.CallId == callId)
                    {
                        if (!match)
                        {
                            match = true;
                        }
                        else
                        {
                            // Second match, call-id not unique.
                            return false;
                        }
                    }
                }
            }

            return match;
        }
    }
}
